为什么 Rust 的 async trait 不是 object-safe 的

#1. Object Safety 的基本要求

要让 trait 可以用作 trait object (dyn Trait),必须满足 object safety 规则:

trait ObjectSafe {
    fn method(&self) -> String;     // ✅ 有 receiver
    // fn generic<T>(&self) -> T;   // ❌ 不能有泛型参数
    // fn returns_self(&self) -> Self; // ❌ 不能返回 Self
    // const CONST: i32;            // ❌ 不能有关联常量
}

核心原理:动态派发通过 vtable(虚函数表)实现,vtable 必须在编译时确定每个方法的确切签名。

#2. Async 函数的实际机制

// 你写的代码:
async fn foo(&self) -> String {
    tokio::time::sleep(Duration::from_millis(100)).await;
    "hello".to_string()
}

// 编译器实际生成的:
fn foo(&self) -> impl Future<Output = String> + '_ {
    // 返回一个编译器生成的唯一状态机类型
    // 比如:FooFuture_A_12345 { state: ..., self_ref: ... }
}

关键问题:每个 async fn 都生成一个唯一的、不同的状态机类型。

#3. 冲突的根本原因

trait AsyncTrait {
    async fn method(&self) -> String;
}

struct ServiceA;
struct ServiceB;

impl AsyncTrait for ServiceA {
    async fn method(&self) -> String {
        "A".to_string()  // 立即返回
    }
}

impl AsyncTrait for ServiceB {
    async fn method(&self) -> String {
        tokio::time::sleep(Duration::from_millis(100)).await;
        "B".to_string()  // 需要异步等待
    }
}

编译器为这两个实现生成完全不同的 Future 类型:

// ServiceA 生成类似这样的类型:
struct ServiceAMethodFuture {
    // 立即完成,可能只需要一个状态位
    state: u8,
}

// ServiceB 生成类似这样的类型:
struct ServiceBMethodFuture {
    // 需要存储 Timer 状态和更复杂的状态机
    timer: Option<tokio::time::Sleep>,
    state: ComplexState,
    // ... 更多字段
}

#4. Vtable 无法统一

当尝试创建 dyn AsyncTrait 时:

// 这样做会失败:
let services: Vec<Box<dyn AsyncTrait>> = vec![
    Box::new(ServiceA),
    Box::new(ServiceB),
];

// 因为 vtable 不知道 method() 应该返回什么类型:
// ServiceA::method 返回 ServiceAMethodFuture
// ServiceB::method 返回 ServiceBMethodFuture
// 这两个类型完全不兼容!

Vtable 需要统一的方法签名,但 impl Future<Output = String> 对每个实现都是不同的具体类型。

#5. 解决方案的原理

// 方案1:Box<dyn Future> 类型擦除
trait CoreService {
    fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

impl CoreService for ServiceA {
    fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
        Box::pin(async { "A".to_string() })
        //       ^^^^^^^^^^^^^^^^^^^^^^^^
        //       不同的Future类型被擦除到相同的trait object
    }
}

impl CoreService for ServiceB {
    fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
        Box::pin(async {
            tokio::time::sleep(Duration::from_millis(100)).await;
            "B".to_string()
        })
        //   ^^^^^^^^^^^^
        //   也被擦除到相同的trait object
    }
}

现在 vtable 可以统一了:

// Vtable 中的方法签名:
fn method(*const (), ...) -> Pin<Box<dyn Future<Output = String> + Send>>;
//                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                           所有实现都返回相同的类型!

#6. 为什么 async_trait crate 能工作?

async_trait 本质上就是自动做了我们手动做的事情:

#[async_trait]
trait AsyncTrait {
    async fn method(&self) -> String;
}

// 展开后变成:
trait AsyncTrait {
    fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

总结

根本原因:Rust 的类型系统要求 trait objects 的所有方法在编译时有确定的、统一的签名,但 async fn 为每个实现生成不同的 Future 类型,违反了这个要求。

解决思路:通过类型擦除(Box<dyn Future>)将不同的 Future 类型统一到相同的接口后面,让 vtable 可以工作。